﻿using Devil;
using System.Collections.Generic;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;

namespace GameToolkit
{
    using MaterialInst = MaterialCache.MaterialInst;

    [DisallowMultipleComponent, RequireComponent(typeof(Renderer)), ExecuteAlways]
    public sealed class OverrideMaterialInstance : MaterialOverrider
    {
        public static int GetMatInstID(int groupId, Material mat)
        {
            return MathExt.Hash(mat.GetInstanceID(), groupId);
        }

        public enum EMaterialLife
        {
            OnlyFeatureOn,
            IsActiveAndEnabled,
            Persistent,
        }

        [System.Serializable]
        struct MaterialSrc
        {
            public int index;
            public Material material;
            public MaterialSrc(int index, Material mat)
            {
                this.index = index;
                this.material = mat;
            }

            public override string ToString()
            {
                if (MainThread.IsMainThread && material != null)
                    return string.Format("[{0}] {1}", index, material.name);
                else
                    return string.Format("srcMat[{0}]", index);
            }
        }

        [SerializeField]
        //bool m_KeepMaterialAlive;
        EMaterialLife m_MaterialLife;

        [SerializeField, HideInInspector]
        MaterialSrc[] m_Materials;

        MaterialSrc[] mMatIndex;
        MaterialInst[] mMatBlocks;
        bool mIsRegisted;
        bool mIsInitialized;
        MaterialGroup mGroup;
        public MaterialGroup group => mGroup;


#if UNITY_EDITOR
        bool mIsPrefab;

        [MenuItem("CONTEXT/Renderer/Override Materials")]
        static void OverrideRenderers(MenuCommand cmd)
        {
            var r = cmd.context as Renderer;
            var m = r.GetComponent<OverrideMaterialInstance>();
            if (m == null)
            {
                m = r.gameObject.AddComponent<OverrideMaterialInstance>();
                var mats = r.sharedMaterials;
                m.m_Materials = new MaterialSrc[mats.Length];
                for (int i = 0; i < mats.Length; i++)
                {
                    m.m_Materials[i] = new MaterialSrc(i, mats[i]);
                }
#if UNITY_EDITOR
                EditorUtility.SetDirty(r.gameObject);
                EditorUtility.SetDirty(m);
#endif
            }
        }
#endif

        public override Material[] sharedMaterials
        {
            get
            {
                var r = AttachedRenderer;
                if (mIsRegisted && r != null)
                {
                    var lst = CacheUtil.GetListCahce<Material>();
                    r.GetSharedMaterials(lst);
                    for (int i = 0; i < m_Materials.Length; i++)
                    {
                        var m = m_Materials[i];
                        if (m.material != null)
                            lst[m.index] = m.material;
                    }
                    return lst.ToArray();
                }
                return base.sharedMaterials;
            }
        }

        //public override int MaterialCount => mIsRegisted ? mMatIndex.Length : base.MaterialCount;

        public override Material GetMaterial(int index)
        {
            if (mIsRegisted)
            {
                for (int i = 0; i < mMatIndex.Length; i++)
                {
                    var m = mMatIndex[i];
                    if (m.index == index)
                        return m.material;
                }
            }
            return base.GetMaterial(index);
        }

        public override void SetMaterial(int index, Material mat)
        {
            var dirty = false;
            if (mIsInitialized)
            {
                bool fallback = true;
                for (int i = 0; i < mMatIndex.Length; i++)
                {
                    var src = mMatIndex[i];
                    if (src.index == index)
                    {
                        dirty = src.material != mat;
                        src.material = mat;
                        if (dirty)
                            mMatIndex[i] = src;
                        fallback = false;
                        // set to m_Materials
                        if (i < m_Materials.Length && m_Materials[i].index == index)
                        {
                            m_Materials[i] = src;
                        }
                        else
                        {
                            for (int k = 0; k < m_Materials.Length; k++)
                            {
                                var m = m_Materials[k];
                                if (m.index == index)
                                {
                                    m_Materials[k] = src;
                                    break;
                                }
                            }
                        }
                        
                        break;
                    }
                }
                if (dirty)
                {
                    if (mIsRegisted)
                    {
                        SetFeatureOn();
                        if (Overrider != null)
                            Overrider.SetDirty(this);
                    }
                    else
                    {
                        SetFeatureOff();
                    }
                }
                else if (fallback)
                {
                    base.SetMaterial(index, mat);
                }
            }
            else
            {
                for (int i = 0; i < m_Materials.Length; i++)
                {
                    var m = m_Materials[i];
                    if (m.index == index)
                    {
                        dirty = m.material != mat;
                        m.material = mat;
                        m_Materials[i] = m;
                        break;
                    }
                }
                base.SetMaterial(index, mat);
            }

#if UNITY_EDITOR
            if (dirty)
                EditorUtility.SetDirty(this);
#endif
        }

        bool TryInitialize()
        {
            if (!mIsInitialized)
            {
                if (AttachedRenderer != null && m_Materials != null && m_Materials.Length > 0)
                {
                    var index = CacheUtil.GetListCahce<MaterialSrc>();
                    for (int i = 0; i < m_Materials.Length; i++)
                    {
                        var src = m_Materials[i];
                        if (src.material != null)
                        {
                            index.Add(src);
                        }
                    }
                    if (mMatIndex == null || mMatIndex.Length != index.Count)
                    {
                        mMatIndex = index.ToArray();
                    }
                    else
                    {
                        index.CopyTo(mMatIndex, 0);
                    }
                    if (mMatBlocks == null || mMatBlocks.Length != mMatIndex.Length)
                    {
                        var lst = mMatBlocks;
                        mMatBlocks = new MaterialInst[mMatIndex.Length];
                        if (lst != null)
                        {
                            for (int i = 0; i < lst.Length; i++)
                            {
                                var m = lst[i];
                                if (m != null)
                                    m.Release();
                            }
                        }
                    }
                }
                mIsInitialized = mMatBlocks != null && mMatBlocks.Length > 0;
            }
            return mIsInitialized;
        }

        protected override void OnFeatureEnabled()
        {
            mIsRegisted = TryInitialize();
            if (mIsRegisted)
            {
                SetFeatureOn();
            }
        }

        protected override void OnFeatureDisabled()
        {
            if (mIsRegisted)
            {
                mIsRegisted = false;
                SetFeatureOff();
            }
        }

        void SetFeatureOn()
        {
            var mats = CacheUtil.GetListCahce<Material>();
            var r = AttachedRenderer;
            r.GetSharedMaterials(mats);
            int groupId;
            if (mGroup != null)
                groupId = mGroup.GroupID;
            else
                groupId = Overrider == null ? 0 : Overrider.GetInstanceID();
            for (int i = 0; i < mMatBlocks.Length; i++)
            {
                var src = mMatIndex[i];
                var id = src.material == null ? 0 : GetMatInstID(groupId, src.material);
                var mat = mMatBlocks[i];
                if (mat == null || mat.id != id)
                {
                    var prev = mat;
                    if (src.material != null)
                    {
                        mat = MaterialCache.GetMatInstance(id, src.material, groupId);
                        mat.Retain();
                    }
                    else
                    {
                        mat = null;
                    }
                    mMatBlocks[i] = mat;
                    if (prev != null)
                    {
                        prev.Release();
                    }
                }
                if (mat != null)
                {
#if UNITY_EDITOR
                    mat.SetAsPrefab(mIsPrefab, r, src.index);
#endif
                    mats[src.index] = mat._material;
                }
            }
#if UNITY_EDITOR
            if (!mIsPrefab)
#endif
                r.sharedMaterials = mats.ToArray();

        }

        void SetFeatureOff()
        {
            var mats = CacheUtil.GetListCahce<Material>();
            var r = AttachedRenderer;
            r.GetSharedMaterials(mats);
#if UNITY_EDITOR
            if (mIsPrefab)
            {
                for (int i = 0; i < mMatBlocks.Length; i++)
                {
                    var m = mMatBlocks[i];
                    if (m != null)
                        m.SetAsPrefab(false, r, mMatIndex[i].index);
                }
            }
#endif
            for (int i = 0; i < mMatIndex.Length; i++)
            {
                var src = mMatIndex[i];
                if (src.material != null)
                {
                    mats[src.index] = src.material;
                }
            }
            r.sharedMaterials = mats.ToArray();
            if (m_MaterialLife == EMaterialLife.OnlyFeatureOn)
            {
                for (int i = 0; i < mMatBlocks.Length; i++)
                {
                    var m = mMatBlocks[i];
                    if (m != null)
                    {
                        mMatBlocks[i] = null;
                        m.Release();
                    }
                }
            }
        }

        internal override void ResetMaterialCbuffer()
        {
            if (mIsRegisted)
            {
                for (int i = 0; i < mMatBlocks.Length; i++)
                {
                    var mat = mMatBlocks[i];
                    if (mat != null)
                    {
                        mat.Clear();
                    }
                }
            }
        }

        internal override void SetupMaterialCbuffer(ICbufferBlock cbuffer, HashSet<int> idCache)
        {
            if (mIsRegisted)
            {
                for (int i = 0; i < mMatBlocks.Length; i++)
                {
                    var inst = mMatBlocks[i];
                    if (inst != null && idCache.Add(inst.id))
                    {
                        var src = mMatIndex[i];
                        cbuffer.OnSetupMaterial(AttachedRenderer, src.index, inst);
                    }
                }
            }
        }

        internal override void SubmitMaterialCbuffers()
        {
#if UNITY_EDITOR
            for (int i = 0; i < mMatBlocks.Length; i++)
            {
                var inst = mMatBlocks[i];
                if (inst != null)
                {
                    inst.FlushMPBValues();
                }
            }
#endif
        }

        protected override void OnEnable()
        {
#if UNITY_EDITOR
            mIsPrefab = !Application.isPlaying || GlobalUtil.IsPrefabStage(gameObject);
#endif
            base.OnEnable();
            mGroup = GetMatGroup();
        }

        protected override void OnDisable()
        {
            base.OnDisable();
            if (m_MaterialLife == EMaterialLife.IsActiveAndEnabled && mMatBlocks != null)
            {
                for (int i = 0; i < mMatBlocks.Length; i++)
                {
                    var m = mMatBlocks[i];
                    if (m != null)
                    {
                        mMatBlocks[i] = null;
                        m.Release();
                    }
                }
            }
        }

        protected override void OnDestroy()
        {
            base.OnDestroy();
            mIsInitialized = false;
            mIsRegisted = false;
            mMatIndex = null;
            mGroup = null;
            if (mMatBlocks != null)
            {
                var lst = mMatBlocks;
                mMatBlocks = null;
                for (int i = 0; i < lst.Length; i++)
                {
                    var m = lst[i];
                    if (m != null)
                        m.Release();
                }
            }
        }

        MaterialGroup GetMatGroup()
        {
            var grp = GetComponentInParent<MaterialGroup>();
            if (grp != null && (Overrider == null || !grp.transform.IsChildOf(Overrider.transform)))
                return null;
            else
                return grp;
        }

        protected override void ResetTransformParentIfNecessary()
        {
            base.ResetTransformParentIfNecessary();
            var grp = GetMatGroup();
            if (grp != mGroup)
            {
                mGroup = grp;
                if (mIsRegisted)
                    SetFeatureOn();
            }
        }

#if UNITY_EDITOR

        public override void OnSelectedAsPreview()
        {
            base.OnSelectedAsPreview();
            if (!IsFeatureOn)
            {
                var mats = CacheUtil.GetListCahce<Material>();
                var r = AttachedRenderer;
                r.GetSharedMaterials(mats);
                var len = m_Materials == null ? 0 : m_Materials.Length;
                for (int i = 0; i < len; i++)
                {
                    var m = m_Materials[i];
                    if (m.material != null && m.index < mats.Count && m.index >= 0)
                    {
                        mats[m.index] = m.material;
                    }
                }
                r.sharedMaterials = mats.ToArray();
            }
        }

        protected override void OnValidateDuringEdit(bool isActive)
        {
            if (mGroup == null)
                mGroup = null;
            mIsInitialized = false;
            if (isActive && Overrider != null)
            {
                var len1 = mMatIndex == null ? 0 : mMatIndex.Length;
                var len2 = m_Materials == null ? 0 : m_Materials.Length;
                var dirty = len1 != len2;
                var grp = GetComponentInParent<MaterialGroup>();
                dirty |= grp != mGroup;
                for (int i = len2 - 1; !dirty && i >= 0; i--)
                {
                    if (m_Materials[i].index != mMatIndex[i].index || m_Materials[i].material != mMatIndex[i].material)
                        dirty = true;
                }
                if (dirty)
                {
                    Overrider.Unregist(this);
                    if (mMatBlocks != null)
                    {
                        for (int i = 0; i < mMatBlocks.Length; i++)
                        {
                            var m = mMatBlocks[i];
                            if (m != null)
                            {
                                m.Release();
                            }
                        }
                    }
                    mMatIndex = null;
                    mMatBlocks = null;
                    Overrider.Regist(this);
                }
            }
        }
#endif

    }
}
